iT邦幫忙

2023 iThome 鐵人賽

DAY 4
0
自我挑戰組

Unit Test 學習路系列 第 4

Day 3: Unit Test/Integration Test/E2E Test 的快速實作。

  • 分享至 

  • xImage
  •  

昨天了解了主要三個測試類型:Unit Test, Integration Test, E2E Test
但是了解理論還是很模糊,所以今天我找了一個學習影片,用簡單的例子來快速實作三種測試類型。

實作學習搭配:JavaScript Testing Introduction Tutorial - Unit Tests, Integration Tests & e2e Tests


  • 事前準備
  • Unit Test
  • Integration Test
  • E2E Test

事前準備

需要的依賴

目前資料結構

|-- dist: 存放 webpack 打包後的檔案內容。
|-- index.html: 網頁畫面。
|-- app.js: 存取 index.html 的 DOM,操作畫面邏輯。
|-- util.js: 操作邏輯會用到的 function。
|-- ...

修改測試指令

{
  "scripts": {
    "test": "jest"
    "auto-test": "jest --watch" // 執行一次後,後面有修改程式碼,都會自動執行測試
  },
}

目前執行畫面

兩個 Input 加上一個按鈕,點擊按鈕產生 User列表。
https://ithelp.ithome.com.tw/upload/images/20230919/20131689R77glC3hSv.png
學習畫面來源

準備好就開始吧!


Unit Test

  1. 測試方向:我想測試輸入兩個 Input,Output 結果是否符合預期。
  2. 測試目標:generateText function。
    exports.generateText = (name, age) => {
      // Returns output text
      return `${name} (${age} years old)`;
    };
    
  3. 撰寫測試:準備好測試資料夾:util.test.js
    (Jest 會自動讀取 OOO.test.js 或 OOO.spec.js 進行測試)
     // 引入要測試的資料夾位置與測試目標
     const { generateText } = require("./util");
    
      // 使用 jest 提供的測試方法:test(測試目標, 執行測試函式)
     test("Expect: Output name and age string", () => {
         const testOutput = generateText("Joanna", 18);
    
         // expect(測試項目).<選擇執行方法>(預期執行結果)
         expect(testOutput).toBe("Joanna (18 years old)");
     });
    

補充:toBe()
用途:使用Object.is() 比對基礎型別或物件型別兩者是否相等。
參考來源:https://jestjs.io/docs/expect#tobevalue

  1. 執行測試結果:執行測試指令yarn test

    成功結果:

    https://ithelp.ithome.com.tw/upload/images/20230919/20131689nKRFQGyVng.png

    失敗結果:

    https://ithelp.ithome.com.tw/upload/images/20230919/20131689ThuLLQueYF.png

  2. 補充反向錯誤測試結果:如果 generateText() 應該要傳入參數,而沒傳入的情況。

    test("Expect: input string w/ undefined", () => {
        const testOutput = generateText();
        expect(testOutput).toBe("undefined (undefined years old)");
    });
    

我們可以針對同一個函式測試多種情況,也可以在同一支 測試函式test()中寫入多個input與 預期expect()


Integration Test

  1. 測試方向:我想測試輸入兩個 Input,經過檢查機制後,Output 結果是否符合預期。

  2. 測試目標:AddUser 內的 validateInput()generateText()

        const addUser = () => {
          // Fetches the user input, creates a new HTML element based on it
          // and appends the element to the DOM
          const newUserNameInput = document.querySelector('input#name');
          const newUserAgeInput = document.querySelector('input#age');
    
          // ------------------------ 測試項目1 ------------------------ 
          if (
            !validateInput(newUserNameInput.value, true, false) ||
            !validateInput(newUserAgeInput.value, false, true)
          ) {
            return;
          }
          // ------------------------ 測試項目1 ------------------------ 
    
          const userList = document.querySelector('.user-list');
    
          // ------------------------ 測試項目2 ------------------------ 
          const outputText = generateText(
            newUserNameInput.value,
            newUserAgeInput.value
          );
          // ------------------------ 測試項目2 ------------------------ 
    
          const element = createElement('li', outputText, 'user-item');
          userList.appendChild(element);
        };
    
  3. 先進行重構,把上面要進行的測試項目拆出,方便後續測試:
    ./utils.js : 包含兩個函式邏輯。

    exports.checkAndGenerate = (name, age) => {
      if (
        !validateInput(name, true, false) ||
        !validateInput(age, false, true)
      ) {
        return false;
      }
    
      return generateText(name, age);
    }
    

    app.js

    const addUser = () => {
      // Fetches the user input, creates a new HTML element based on it
      // and appends the element to the DOM
      const newUserNameInput = document.querySelector('input#name');
      const newUserAgeInput = document.querySelector('input#age');
    
      // ---------------------------- 重構內容 ---------------------------- 
      const outputText = checkAndGenerate(   
        newUserNameInput.value,
        newUserAgeInput.value
      )
      if(!outputText){
        return false
      }
      // ---------------------------- 重構內容 ---------------------------- 
    
      const userList = document.querySelector('.user-list');
      const element = createElement('li', outputText, 'user-item');
      userList.appendChild(element);
    };
    
  4. 撰寫測試:

    const { checkAndGenerate } = require("./util");
    test("Expext: checkValue & return results", () => {
        const testOutput1 = checkAndGenerate("Joanna", 18);
        expect(testOutput1).toBe("Joanna (18 years old)")

        const testOutput2 = checkAndGenerate();
        expect(testOutput2).toBe(false);
    })

執行結果:
https://ithelp.ithome.com.tw/upload/images/20230919/20131689hdq1hsmSUi.png

補充:

這裡的 Integration Test 目標checkAndGenerate() 可以看到,裡面包含兩個函式:validateInput()generateText()。假設我將兩個函式 都進行 Unit Test 通過,但這只能證明兩個函式I/O 都符合預期,無法保證 Integration Test 不會有問題。像是這段:

if (
    !validateInput(name, true, false) ||
    !validateInput(age, false, true)
  ) {
    return false;
  }

如果 if-else 邏輯寫錯,Integration Test 也不會測試成功(不符合預期)。


E2E Test

  1. 測試方向:我想測試在瀏覽器上輸入兩個 Input,Output 結果是否符合預期。

  2. 測試目標:使用 Poppeteer 操作 DOM,模擬使用者行為,查看輸出結果。

  3. 撰寫測試:
    Poppeteer 模擬操作:

    • 啟動模擬瀏覽器:puppeteer.launch()
    • 開啟新頁面
    • 前往指定頁面網址
    • 點擊輸入框(取得 DOM 節點)
    • 輸入內容(取得 DOM 節點)
    • 點擊 Add User 按鈕(取得 DOM 節點)
    • [測試] 取得輸出文字內容
    • [測試] 確認輸出文字內容是否符合預期
        test("Expect: user type inputs & click to get results", async () => {
        const browser = await puppeteer.launch({
            slowMo: 80,
            headless: false, // 代表我想看到 puppeteer 開啟 瀏覽器 的 GUI
        });
    
        const page = await browser.newPage();
        // 帶入 Live Demo 路徑,或 檔案路徑
        await page.goto("http://127.0.0.1:5500/index.html"); 
        await page.click("input#name");
        await page.type("input#name", "Joanna");
        await page.click("input#age");
        await page.type("input#age", '18');
        await page.click("button#btnAddUser");
    
        const textContent = await page.$eval(".user-item", el => el.textContent);
        expect(textContent).toBe("Joanna (18 years old)");
    });
    

執行結果:https://ithelp.ithome.com.tw/upload/images/20230919/20131689Qfl4K6ggrw.png

補充:

  • Puppeteer 執行每個步驟都會回傳 Promise,記得要加 async await
  • 輸入數字的 Input,value 值一樣給 string,不然 Puppeteer 會停在一半,無法往下進行。
  • $eval(<選取器>, 操作函式):擷取網頁上的一數據。

今天的練習很充實,寫了 code 更有感覺,三種類型的測試都有明確的測試目標,需要看專案上實際功能,才能提高測試價值。而用「好寫測試」的方向去重構程式碼,確實比我自己埋頭思考怎麼寫出簡潔程式碼有頭緒多了!
但我在寫 test() 有一點點疑惑,需要自己假設使用者不同的輸入寫法做測試,但使用者輸入的可能性是我沒想到的情況,就很有可能測試有漏網之魚吧!

Anyway~且戰且走,邊走邊看吧~ /images/emoticon/emoticon29.gif


參考資源


上一篇
Day 2: Unit Test/Integration Test/E2E Test 的區別。
下一篇
Day4: 學習 TDD(測試驅動開發)的基本原則和流程。
系列文
Unit Test 學習路31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言